using System;
using System.Drawing;
using System.Diagnostics;

using Clandestine.Base;
using Clandestine.Graphics;

namespace Shlick
{
	public class CameraController
	{
		private enum Mode
		{
			GotoPoint,              // Camera possessed - can't exit this mode until we get to the defined point.
			RelativeMovementLoose,  // Shift not pressed + WASD key movement: Fast 'n' loose, non-grid snapping movement
			RelativeMovementSnapped // Shift pressed + WASD key movement: Grid-snapped movement.
		}
		
		// Note that in RelativeMovementSnapped mode we actually do camera movement in the KeyDown handler,
		//  so as to take advantage of the OS' key-repeat settings.
		// This is unlike RelativeMovementLoose, where the KeyDown and KeyUp handlers track which keys are being pressed
		//  and then smooth camera movement is performed in the Update() method.
		
		public MapDocument MapDocument { get; private set; }
		public Camera Camera { get; private set; }
		
		// Gets the current map tile size, or '1' if the map isn't ready yet.
		private int tileSize
		{
			get { return (MapDocument.Map != null) ? MapDocument.Map.GridSize : 1; }
		}
		
		private Mode mode;
		
		private Stopwatch sw;
		
		const float gotoDurationMax = 1.0f;
		const float gotoDurationMin = 0.2f;
		const int gotoMaxScaleRange = 1600;
		const int gotoMinScaleRange = 200;
		float gotoProgress = 0f;
		Point gotoPoint;
		
		// 'Loose' relative WASD movement keys speed tuning constants
		const float velocityMultiplier = 500f;  // maximum 'loose' speed
		const float velMaxTime = 0.4f; // how long we take to reach max speed
		const float velocityDecMultiplier = velocityMultiplier; //16f;
		const float velDecMaxTime = velMaxTime; // how long we take to go from max speed to 0
		
		// is the up/down/left/right/shift key pressed?
		bool ru, rd, rl, rr, sh;
		float relXVelocity = 0f;  // current relative 'loose' X velocity
		float relYVelocity = 0f;  // current relative 'loose' Y velocity

		//float tilesXPosAccum = 0f;
		//float tilesYPosAccum = 0f;
		//const float tilesPerSec = 3.5f;
		
		//
		// NOTE TO SELF: To stop CC controlling camera during run, swap out Graphics.Camera for a new one!
		//               Don't do something failure-tastic instead!
		//
		
		public CameraController(MapDocument md, Camera camera)
		{
			this.MapDocument = md;
			this.Camera = camera;
			shiftMode(Mode.RelativeMovementLoose);
			sw = new Stopwatch();
			sw.Start();
		}
		
		private void shiftMode(Mode newMode)
		{
			gotoProgress = 0f;
			gotoPoint = Point.Empty;
			relXVelocity = 0f;
			relYVelocity = 0f;
			
			switch (newMode)
			{
				case Mode.RelativeMovementSnapped:
					this.Camera.Position.X = this.Camera.Position.X - (this.Camera.Position.X % tileSize);
					this.Camera.Position.Y = this.Camera.Position.Y - (this.Camera.Position.Y % tileSize);
					break;
			}
			
			this.mode = newMode;
		}
		
		public void Reset()
		{
			Camera.Position = PointF.Empty;
			shiftMode(Mode.RelativeMovementLoose);
		}
		
		public void KeyDown(bool up, bool down, bool left, bool right, bool sh)
		{
			if (sh && !this.sh) // Shift key pressed and shift key was not pressed before now?
			{
				if (mode != CameraController.Mode.GotoPoint) // Never interrupt a GotoPoint.
					shiftMode(Mode.RelativeMovementSnapped);
				this.sh = true;
			}
			
			if (up)
				this.ru = true;
			if (down)
				this.rd = true;
			if (left)
				this.rl = true;
			if (right)
				this.rr = true;
			
			if (mode == CameraController.Mode.RelativeMovementSnapped)
			{
				if ((this.ru || this.rd) && !(this.ru && this.rd))
					this.Camera.Position.Y += this.rd ? tileSize : -tileSize;
				if ((this.rl || this.rr) && !(this.rl && this.rr))
					this.Camera.Position.X += this.rr ? tileSize : -tileSize;
			}
			
			//Log.w("DOWN " + up.ToString() + down.ToString() + left.ToString() + right.ToString() + sh.ToString());
		}
		
		public void KeyUp(bool up, bool down, bool left, bool right, bool sh)
		{
			if (sh && this.sh) // Shift key released, but shift key was pressed til now?
			{
				if (mode != CameraController.Mode.GotoPoint) // NEVER interrupt a GotoPoint.
					shiftMode(Mode.RelativeMovementLoose);
				this.sh = false;
			}
			
			if (up)
				this.ru = false;
			if (down)
				this.rd = false;
			if (left)
				this.rl = false;
			if (right)
				this.rr = false;
			
			//Log.w("UP " + up.ToString() + down.ToString() + left.ToString() + right.ToString() + sh.ToString());
		}
		
		private float relVelScale(float relin, bool pos, bool neg, double dts)
		{
			bool relinIsPos = relin > 0f;
			
			// For X axis, 'pos' is right-key-is-pressed? and 'neg' is left-key-is-pressed?
			// For Y axis, 'pos' is down-key-is-pressed? and 'neg' is up-key-is-pressed?
			
			// What the following block of code does:
			// ---------
			//  1. If pos OR neg is pressed (but NOT both)...
			//  |--- If current velocity is +ve but neg is true, or velocity is -ve but pos is true...
			//  |  |---- Flip velocity sign and half velocity magnitude.
			//  |         (This means if the user pressed right then switches to left, they don't have to wait
			//  |          for the camera to go from 0 speed (or even worse, reverse direction), but they also
			//  |          don't have to contend with the camera racing at max speed in a diff direction yet.)
			//  |
			//  |--- If pos, add (acceleration per second) * (seconds passed since last Update())
			//  |    If neg, subtract that stuff instead.
			//  |
			//  |--- Clamp velocity between (max negative velocity) and (max positive velocity).
			//
			//  2. OTHERWISE, if the magnitude of the velocity is greater than 1...
			//  |   (so, either neither the pos or neg keys are pressed  or both the pos and neg keys are being pressed)
			//  |--- If vel +ve, Subtract (deceleration per second) * (seconds passed since last Update()) from velocity
			//  |    If vel -ve, Add that stuff instead.
			//  |
			//  |--- Did the above step cause the sign of velocity to change from what it was before?
			//     |--- If so, just set velocity to 0.
			//
			// 3. OTHERWISE (so neither pos nor neg keys are pressed or both pos and neg keys are pressed,
			//                AND the magnitude of velocity is <= 1)
			// |--- Just set the velocity to 0, for goodness' sake.
			
			if ((pos || neg) && !(pos && neg))
			{
				// Accelerate! (with caps of/c)
				
				// Hm. Did they just switch direction suddenly?
				if ((neg && relinIsPos) || (pos && !relinIsPos))
					relin = -relin / 2f;
				
				relin += (float) (velocityMultiplier / velMaxTime * dts * (pos ? 1 : -1));
				if (relin < -velocityMultiplier)
					relin = -velocityMultiplier;
				if (relin > velocityMultiplier)
					relin = velocityMultiplier;
			}
			else if (Math.Abs(relin) > 1f)
			{
				// Decelerate.
				relin -= (float) ((velocityDecMultiplier / velDecMaxTime) * dts * (relinIsPos ? 1 : -1));
				if (relinIsPos != (relin > 0f))
					relin = 0f;
			}
			else
				relin = 0f; // Zero the negligible speed.
			
			return relin;
		}
		
		public void Update()
		{
			TimeSpan delta = sw.Elapsed;
			sw.Reset();
			sw.Start();
			
			switch (mode)
			{
				case Mode.GotoPoint:
					throw new NotImplementedException();
					// if (goto finished nao)
					//	shiftMode(sh ? Mode.RelativeMovementSnapped : Mode.RelativeMovementLoose);
					break;
					
				case Mode.RelativeMovementLoose:
					relXVelocity = relVelScale(relXVelocity, rr, rl, delta.TotalSeconds);
					relYVelocity = relVelScale(relYVelocity, rd, ru, delta.TotalSeconds);
					Camera.Position.X += (float) (relXVelocity * delta.TotalSeconds);
					Camera.Position.Y += (float) (relYVelocity * delta.TotalSeconds);
					break;
			}
		}
	}
}

